如果程式剛初始化的時候發生crash,可能還沒有太大傷害,但若程式執行起來已經上線一陣子,正在處理到一半的資料突然中斷,麻煩可就大了。
多年來的開發經驗,心得就是:沒有什麼是保證絕對不會壞。
該壞的就壞,你可能無法做些什麼,但是可以的話,控制住損害範圍,保護無辜或者不相關的部分。
舉個例子,API server 服務進來的request,這些request彼此不相關,各自獨立也不互相影響。倘若其中一個request的程式處理會導致server crash掉,那麼將直接導致目前所有正在處理的request也跟著crash,後續的request也因為server死掉,完全無法處理,整體而言實在糟糕至極。
相反,若只有該request會發生問題,或者某個api因為某種情況而無法正常服務,卻不會影響其他功能,體驗上就會好很多。
因為有些結構或者實體在執行期間才會建立,或者進行了某些處理,結構裡面才有資料。
於是在發生了某了錯誤,可能導致結構無法如預期實體化,你卻使用這個struct的某個method,或者資料處理沒有按預想的發展,而強硬地用index取撈取等等,可能都會發生讓程式視為嚴重的錯誤,往外拋出panic。
這些錯誤除了race condition可以在build前下指令偵測以外,儘量以防衛性的方式撰寫程式碼,剩下的還是加上recover比較保險。
recover 一定要搭配defer才能發揮作用,不可單獨只使用recover,然後recover的程式碼有寫但還沒執行到的話,就遇到崩潰也沒路用(有時候運氣就是這麼不好,諸君千萬別心存僥倖)。
策略上通常會想要在最不可能崩潰或出問題的地方,先執行過這段recover程式碼確保安全,所以做recover處理的程式區塊,往往擺在function進入的一開始。
寫法如下
defer func() {
if err := recover(); err != nil {
fmt.Println("Error:", err)
// 或者自定義的處理
}
}()
但由於panic已經被你寫的recover接到,所以預設就不會打印有發生問題的trace,自己加上問題點才好找。
defer func() {
if err := recover(); err != nil {
fmt.Println("Error:", err)
log.Println("stacktrace from panic: \n" + string(debug.Stack()))
}
}()
這分為兩個面向討論
panic 並非萬惡之物,有recover並不代表程式沒有問題,或者就解決問題,若在深思熟慮的規劃之下,竟然還有未知的原因,造成panic跳出的時候,要不要用recover接起來,其實看各位專案的需求。
我們的專案開發,傾向在初始化時,譬如說DB連線建立,發生重大的錯誤直接讓panic丟出來,造成程式crash,好處是發佈後的第一時間,我們就知道程式的狀況,連基本的連線都有問題,後續做什麼都是錯的。
而程式順利啟動後,會傾向準備好recover,避免一個部分造成的錯誤,影響到其他正常的環節,執行到一半的資料錯誤事後修補往往非常困難。
若程式有開goroutine處理,要注意recover不寫在goroutine裡面是無效的,是goroutine裡面發生panic的話,即使main function有寫recover,也不會有作用。
如下面小小的範例,recover B 沒把註解打開,發生了panic還是依舊爆炸給你看。
package main
import (
"fmt"
"sync"
)
func main() {
// recover main
defer func() {
if err := recover(); err != nil {
fmt.Println("Error:", err)
}
}()
var wg sync.WaitGroup
testA(&wg)
wg.Wait()
}
func testA(wg *sync.WaitGroup) {
// recover A-1
defer func() {
if err := recover(); err != nil {
fmt.Println("Error:", err)
wg.Done()
}
}()
for i := 0; i < 10; i++ {
// recover A-2
defer func() {
if err := recover(); err != nil {
fmt.Println("Error:", err)
wg.Done()
}
}()
count := i
wg.Add(1)
go testB(wg, count)
}
}
func testB(wg *sync.WaitGroup, count int) {
// recover B
// defer func() {
// if err := recover(); err != nil {
// fmt.Println("Error:", err)
// wg.Done()
// }
// }()
fmt.Println(count)
if count == 5 {
panic("occur panic!")
}
fmt.Println("doing something")
wg.Done()
}
panic("occur panic!")
簡簡單單,使用panic函式就可以自己製造panic,panic一旦產生,接下來的程式就不會繼續執行,而且會持續往外拋,直到有recover接到或者程式crash。
這有點像php的throw,以及搭配try catch,接到exception的感覺。
在很巢狀或深層的函式,如果發生了『error』,又想要將這個錯誤訊息往上傳遞到很外面的時候,在Golang只能一層一層慢慢傳,對於程式撰寫、追蹤、維護上可能會非常非常不方便,所以有人也會採用主動製造panic的方式,直接打到最外面讓安排好的recover接受後,再另做處理。
但是這樣的方式,還是看各自的需求而制定,見仁見智,不能絕對說好或壞。
下篇,筆者跟大家介紹幾個會造成panic的常見情況